// Copyright 2019 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "gpio-light.h"

#include <assert.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <threads.h>
#include <unistd.h>

#include <memory>

#include <ddk/binding.h>
#include <ddk/debug.h>
#include <ddk/metadata.h>
#include <ddk/platform-defs.h>
#include <ddktl/protocol/composite.h>
#include <fbl/alloc_checker.h>

#include <zircon/process.h>
#include <zircon/syscalls.h>
#include <zircon/assert.h>

namespace gpio_light {

zx_status_t GpioLight::MsgGetName(fidl_txn_t* txn, uint32_t index) {
  if (index >= gpio_count_) {
    return fuchsia_hardware_light_LightGetName_reply(txn, ZX_ERR_OUT_OF_RANGE, nullptr, 0);
  }

  if (names_.size() > 0) {
    auto* name = names_.data() + index * kNameLength;
    return fuchsia_hardware_light_LightGetName_reply(txn, ZX_OK, name, strlen(name) + 1);
  } else {
    // Return "gpio-X" if no metadata was provided.
    char name[20];
    snprintf(name, sizeof(name), "gpio-%u\n", index);
    return fuchsia_hardware_light_LightGetName_reply(txn, ZX_OK, name, strlen(name) + 1);
  }
}

zx_status_t GpioLight::MsgGetCount(fidl_txn_t* txn) {
  return fuchsia_hardware_light_LightGetCount_reply(txn, gpio_count_);
}

zx_status_t GpioLight::MsgHasCapability(uint32_t index,
                                        fuchsia_hardware_light_Capability capability,
                                        fidl_txn_t* txn) {
  if (index >= gpio_count_) {
    return fuchsia_hardware_light_LightHasCapability_reply(txn, ZX_ERR_OUT_OF_RANGE, false);
  }
  return fuchsia_hardware_light_LightHasCapability_reply(txn, ZX_OK, false);
}

zx_status_t GpioLight::MsgGetSimpleValue(uint32_t index, fidl_txn_t* txn) {
  if (index >= gpio_count_) {
    return fuchsia_hardware_light_LightGetSimpleValue_reply(txn, ZX_ERR_OUT_OF_RANGE, 0);
  }

  uint8_t value;
  auto status = gpios_[index].Read(&value);
  return fuchsia_hardware_light_LightGetSimpleValue_reply(txn, status, value);
}

zx_status_t GpioLight::MsgSetSimpleValue(uint32_t index, uint8_t value, fidl_txn_t* txn) {
  if (index >= gpio_count_) {
    return fuchsia_hardware_light_LightSetSimpleValue_reply(txn, ZX_ERR_OUT_OF_RANGE);
  }

  auto status = gpios_[index].Write(value);
  return fuchsia_hardware_light_LightSetSimpleValue_reply(txn, status);
}

zx_status_t GpioLight::MsgGetRgbValue(uint32_t index, fidl_txn_t* txn) {
  fuchsia_hardware_light_Rgb rgb = {};
  return fuchsia_hardware_light_LightGetRgbValue_reply(txn, ZX_ERR_NOT_SUPPORTED, &rgb);
}

zx_status_t GpioLight::MsgSetRgbValue(uint32_t index, const fuchsia_hardware_light_Rgb* value,
                                      fidl_txn_t* txn) {
  return fuchsia_hardware_light_LightSetRgbValue_reply(txn, ZX_ERR_NOT_SUPPORTED);
}

static fuchsia_hardware_light_Light_ops_t fidl_ops = {
    .GetName =
        [](void* ctx, uint32_t index, fidl_txn_t* txn) {
          return reinterpret_cast<GpioLight*>(ctx)->MsgGetName(txn, index);
        },
    .GetCount = [](void* ctx,
                   fidl_txn_t* txn) { return reinterpret_cast<GpioLight*>(ctx)->MsgGetCount(txn); },
    .HasCapability =
        [](void* ctx, uint32_t index, fuchsia_hardware_light_Capability capability,
           fidl_txn_t* txn) {
          return reinterpret_cast<GpioLight*>(ctx)->MsgHasCapability(index, capability, txn);
        },
    .GetSimpleValue =
        [](void* ctx, uint32_t index, fidl_txn_t* txn) {
          return reinterpret_cast<GpioLight*>(ctx)->MsgGetSimpleValue(index, txn);
        },
    .SetSimpleValue =
        [](void* ctx, uint32_t index, uint8_t value, fidl_txn_t* txn) {
          return reinterpret_cast<GpioLight*>(ctx)->MsgSetSimpleValue(index, value, txn);
        },
    .GetRgbValue =
        [](void* ctx, uint32_t index, fidl_txn_t* txn) {
          return reinterpret_cast<GpioLight*>(ctx)->MsgGetRgbValue(index, txn);
        },
    .SetRgbValue =
        [](void* ctx, uint32_t index, const fuchsia_hardware_light_Rgb* value, fidl_txn_t* txn) {
          return reinterpret_cast<GpioLight*>(ctx)->MsgSetRgbValue(index, value, txn);
        },
};

zx_status_t GpioLight::DdkMessage(fidl_msg_t* msg, fidl_txn_t* txn) {
  return fuchsia_hardware_light_Light_dispatch(this, txn, msg, &fidl_ops);
}

void GpioLight::DdkRelease() { delete this; }

zx_status_t GpioLight::Create(void* ctx, zx_device_t* parent) {
  fbl::AllocChecker ac;
  auto dev = std::unique_ptr<GpioLight>(new (&ac) GpioLight(parent));
  if (!ac.check()) {
    return ZX_ERR_NO_MEMORY;
  }

  auto status = dev->Init();
  if (status != ZX_OK) {
    return status;
  }

  // devmgr is now in charge of the device.
  __UNUSED auto* dummy = dev.release();
  return ZX_OK;
}

zx_status_t GpioLight::Init() {
  ddk::CompositeProtocolClient composite(parent_);
  if (!composite.is_valid()) {
    zxlogf(ERROR, "GpioLight: Could not get composite protocol\n");
    return ZX_ERR_NOT_SUPPORTED;
  }

  auto component_count = composite.GetComponentCount();
  if (component_count <= 0) {
    return ZX_ERR_INTERNAL;
  }
  // component 0 is platform device, only used for passing metadata.
  gpio_count_ = component_count - 1;

  size_t metadata_size;
  size_t expected = gpio_count_ * kNameLength;
  auto status = device_get_metadata_size(parent(), DEVICE_METADATA_NAME, &metadata_size);
  if (status == ZX_OK) {
    if (expected != metadata_size) {
      zxlogf(ERROR, "%s: expected metadata size %zu, got %zu\n", __func__, expected, metadata_size);
      status = ZX_ERR_INTERNAL;
    }
  }
  if (status == ZX_OK) {
    fbl::AllocChecker ac;
    names_.reset(new (&ac) char[metadata_size], metadata_size);
    if (!ac.check()) {
      return ZX_ERR_NO_MEMORY;
    }

    status =
        device_get_metadata(parent(), DEVICE_METADATA_NAME, names_.data(), metadata_size, &expected);
    if (status != ZX_OK) {
      return status;
    }
  }

  zx_device_t* components[component_count];
  size_t actual;
  composite.GetComponents(components, component_count, &actual);
  if (actual != component_count) {
    return ZX_ERR_INTERNAL;
  }

  fbl::AllocChecker ac;
  auto* gpios = new (&ac) ddk::GpioProtocolClient[gpio_count_];
  if (!ac.check()) {
    return ZX_ERR_NO_MEMORY;
  }
  gpios_.reset(gpios, gpio_count_);

  for (uint32_t i = 0; i < gpio_count_; i++) {
    auto* gpio = &gpios_[i];
    auto status = device_get_protocol(components[i + 1], ZX_PROTOCOL_GPIO, gpio);
    if (status != ZX_OK) {
      return status;
    }
    status = gpio->ConfigOut(0);
    if (status != ZX_OK) {
      zxlogf(ERROR, "gpio-light: ConfigOut failed for gpio %u\n", i);
      return status;
    }
  }

  return DdkAdd("gpio-light", DEVICE_ADD_NON_BINDABLE);
}

static constexpr zx_driver_ops_t driver_ops = []() {
  zx_driver_ops_t ops = {};
  ops.version = DRIVER_OPS_VERSION;
  ops.bind = GpioLight::Create;
  return ops;
}();

}  // namespace gpio_light

ZIRCON_DRIVER_BEGIN(gpio_light, gpio_light::driver_ops, "zircon", "0.1", 4)
BI_ABORT_IF(NE, BIND_PROTOCOL, ZX_PROTOCOL_COMPOSITE),
    BI_ABORT_IF(NE, BIND_PLATFORM_DEV_VID, PDEV_VID_GENERIC),
    BI_ABORT_IF(NE, BIND_PLATFORM_DEV_PID, PDEV_PID_GENERIC),
    BI_MATCH_IF(EQ, BIND_PLATFORM_DEV_DID, PDEV_DID_GPIO_LIGHT), ZIRCON_DRIVER_END(gpio_light)
